/*
 ------------------------------------------------------------------------------
 Copyright (C) 2006-2007 Team Blur.

 This file is part of the Quantum Engine source code.

 The Quantum Engine source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The Quantum Engine source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the Quantum Engine source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


#include "client.h"


#define	CON_LINES			1024
#define CON_LINELEN			(SCREEN_WIDTH / 8) - 2

#define	CON_TIMES			4

#define CON_MAX_INPUT		256

#define CON_MAX_HISTORY		32

#define CON_MAX_CMDS		4096

typedef struct {
	bool			initialized;
	
	// Text buffer
	short			text[CON_LINES][CON_LINELEN];
	int				pos;				// Offset in current line for next print
	int				currentLine;		// Line where next message will be printed
	int				displayLine;		// Bottom of console displays this line
	float			displayFrac;		// Fraction of console to display
	int				colorIndex;			// Current color index
	bool			carriageReturn;		// Carriage return state
	bool			overstrikeMode;		// Overstrike mode

	// Transparent notify lines
	int				notifyTimes[CON_TIMES];

	// Input buffer
	char			inputText[CON_MAX_INPUT];
	int				inputPos;
	int				inputLen;

	// History buffer
	char			historyText[CON_MAX_HISTORY][CON_MAX_INPUT];
	int				historyCount;
	int				historyLine;

	// Command auto-completion
	const char *	cmdList[CON_MAX_CMDS];
	char			cmdArgs[CON_MAX_INPUT];
	int				cmdCount;
	int				cmdIndex;
} console_t;

static console_t	con;

static cvar_t *		con_notifyTime;
static cvar_t *		con_speed;
static cvar_t *		con_noPrint;


/*
 ==================
 Con_ClearNotify
 ==================
*/
static void Con_ClearNotify (void){

	int		i;

	for (i = 0; i < CON_TIMES; i++)
		con.notifyTimes[i] = 0;
}

/*
 ==================
 Con_ClearInput
 ==================
*/
static void Con_ClearInput (void){

	int		i;

	con.inputText[0] = 0;
	con.inputPos = 0;
	con.inputLen = 0;

	// Free the auto-complete list
	for (i = 0; i < con.cmdCount; i++)
		Mem_Free(con.cmdList[i]);

	con.cmdArgs[0] = 0;
	con.cmdCount = 0;
	con.cmdIndex = -1;
}

/*
 ==================
 Con_Linefeed
 ==================
*/
static void Con_Linefeed (void){

	int		i;

	// Scroll down and advance
	if (con.displayLine == con.currentLine)
		con.displayLine++;

	con.currentLine++;

	if (con.displayLine < con.currentLine - CON_LINES + 1)
		con.displayLine = con.currentLine - CON_LINES + 1;
	if (con.displayLine < CON_LINES)
		con.displayLine = CON_LINES;

	// Clear the line
	for (i = 0; i < CON_LINELEN; i++)
		con.text[con.currentLine % CON_LINES][i] = 0;

	// Mark time for transparent overlay
	con.notifyTimes[con.currentLine % CON_TIMES] = cls.frameTime;
}

/*
 ==================
 Con_Print

 Handles cursor positioning, line wrapping, colored text, etc...
 ==================
*/
void Con_Print (const char *text){

	int		c, len;

	if (!con.initialized)
		return;

	while (*text){
		// Set the color index if needed
		if (Str_IsColor(text)){
			con.colorIndex = Str_GetColorIndex(text[1]);
			text += 2;
			continue;
		}

		// Count word length
		for (len = 0; len < CON_LINELEN; len++){
			if (text[len] <= ' ')
				break;
		}

		// Word wrap
		if (len != CON_LINELEN && con.pos + len > CON_LINELEN)
			con.pos = 0;

		if (con.carriageReturn){
			con.carriageReturn = false;

			con.currentLine--;
		}

		if (!con.pos)
			Con_Linefeed();

		c = *text++;

		switch (c){
		case '\n':
			con.pos = 0;
			con.colorIndex = COLOR_DEFAULT;

			break;
		case '\r':
			con.pos = 0;
			con.colorIndex = COLOR_DEFAULT;
			con.carriageReturn = true;

			break;
		default:
			// Display character and advance
			con.text[con.currentLine % CON_LINES][con.pos++] = (con.colorIndex << 8) | c;

			if (con.pos == CON_LINELEN)
				con.pos = 0;

			break;
		}
	}
}

/*
 ==================
 Con_Close
 ==================
*/
void Con_Close (void){

	if (!con.initialized)
		return;

	con.displayFrac = 0.0f;

	Con_ClearNotify();
	Con_ClearInput();

	Key_RemoveCatchers(KEYCATCH_CONSOLE);
}

/*
 ==================
 Con_RunConsole
 ==================
*/
void Con_RunConsole (void){

	float	frac;

	if (!con.initialized)
		return;

	// Decide on the height of the console
	if (Key_GetCatchers() & KEYCATCH_CONSOLE)
		frac = 0.5f;
	else
		frac = 0.0f;

	if (frac < con.displayFrac){
		con.displayFrac -= con_speed->floatValue * MS2SEC(cls.frameMsec);
		if (con.displayFrac < frac)
			con.displayFrac = frac;
	}
	else if (frac > con.displayFrac){
		con.displayFrac += con_speed->floatValue * MS2SEC(cls.frameMsec);
		if (con.displayFrac > frac)
			con.displayFrac = frac;
	}
}


/*
 ==============================================================================

 LINE EDITING

 ==============================================================================
*/


/*
 ==================
 Con_SortCommandList
 ==================
*/
static int Con_SortCommandList (const void *elem1, const void *elem2){

	const char	*cmd1 = *(const char **)elem1;
	const char	*cmd2 = *(const char **)elem2;

	return Str_Compare(cmd1, cmd2);
}

/*
 ==================
 Con_CompleteCommandCallback
 ==================
*/
static void Con_CompleteCommandCallback (const char *found){

	if (con.cmdCount == CON_MAX_CMDS)
		return;

	con.cmdList[con.cmdCount++] = Mem_CopyString(found, TAG_COMMON);
}

/*
 ==================
 Con_CompleteCommand
 ==================
*/
static void Con_CompleteCommand (void){

	cvar_t		*cvar;
	const char	*cmd1, *cmd2;
	char		partial[CON_MAX_INPUT];
	int			i, len;

	// Free the auto-complete list
	for (i = 0; i < con.cmdCount; i++)
		Mem_Free(con.cmdList[i]);

	con.cmdArgs[0] = 0;
	con.cmdCount = 0;
	con.cmdIndex = -1;

	// Tokenize the string
	if (!con.inputText[0])
		return;		// Nothing to search for

	Cmd_TokenizeString(con.inputText);

	// Find matching commands and variables
	Cmd_CompleteCommand(Cmd_Argv(0), Con_CompleteCommandCallback);
	CVar_CompleteVariable(Cmd_Argv(0), Con_CompleteCommandCallback);

	if (!con.cmdCount)
		return;		// Nothing was found

	// Copy the arguments, if available
	Str_Copy(con.cmdArgs, Cmd_Args(1, -1, false), sizeof(con.cmdArgs));

	if (con.cmdCount == 1){
		// Only one was found, so copy it to the input line
		if (con.cmdArgs[0])
			Str_SPrintf(con.inputText, sizeof(con.inputText), "%s %s", con.cmdList[0], con.cmdArgs);
		else
			Str_SPrintf(con.inputText, sizeof(con.inputText), "%s ", con.cmdList[0]);

		con.inputLen = Str_Length(con.inputText);
		con.inputPos = con.inputLen;
	}
	else {
		// Sort the commands
		qsort(con.cmdList, con.cmdCount, sizeof(char *), Con_SortCommandList);

		// Print the commands
		Com_Printf("]%s\n", Cmd_Argv(0));

		for (i = 0; i < con.cmdCount; i++){
			Com_Printf("    %s", con.cmdList[i]);

			cvar = CVar_FindVariable(con.cmdList[i]);
			if (cvar)
				Com_Printf(" = \"%s" S_COLOR_DEFAULT "\"\n", cvar->value);
			else
				Com_Printf("\n");
		}

		// Find the number of matching characters between the first and the
		// last command in the list and copy it
		cmd1 = con.cmdList[0];
		cmd2 = con.cmdList[con.cmdCount-1];

		len = 0;
		while (*cmd1 && *cmd2 && *cmd1 == *cmd2){
			cmd1++;
			cmd2++;

			partial[len] = con.cmdList[0][len];
			len++;
		}
		partial[len] = 0;

		// Copy the match to the input line
		if (con.cmdArgs[0])
			Str_SPrintf(con.inputText, sizeof(con.inputText), "%s %s", partial, con.cmdArgs);
		else
			Str_SPrintf(con.inputText, sizeof(con.inputText), "%s", partial);

		con.inputLen = Str_Length(con.inputText);
		con.inputPos = len;
	}
}

/*
 ==================
 Con_KeyDownEvent
 ==================
*/
void Con_KeyDownEvent (int key){

	const char	*text;
	int			i, j;

	if (!con.initialized)
		return;

	// Auto-complete command
	if (key == K_TAB){
		if (con.cmdCount <= 1)
			Con_CompleteCommand();
		else {
			// Cycle through the commands
			if (++con.cmdIndex == con.cmdCount)
				con.cmdIndex = 0;

			if (con.cmdArgs[0])
				Str_SPrintf(con.inputText, sizeof(con.inputText), "%s %s", con.cmdList[con.cmdIndex], con.cmdArgs);
			else
				Str_SPrintf(con.inputText, sizeof(con.inputText), "%s ", con.cmdList[con.cmdIndex]);

			con.inputLen = Str_Length(con.inputText);
			con.inputPos = con.inputLen;
		}

		return;
	}

	// Any other typing frees the auto-complete list
	for (i = 0; i < con.cmdCount; i++)
		Mem_Free(con.cmdList[i]);

	con.cmdArgs[0] = 0;
	con.cmdCount = 0;
	con.cmdIndex = -1;

	// Toggle console off
	if (key == K_ESCAPE){
		for (i = 0; i < CON_TIMES; i++)
			con.notifyTimes[i] = 0;

		con.inputText[0] = 0;
		con.inputPos = 0;
		con.inputLen = 0;

		Key_RemoveCatchers(KEYCATCH_CONSOLE);

		return;
	}

	// Paste from clipboard
	if (key == K_INS && (Key_IsDown(K_LSHIFT) || Key_IsDown(K_RSHIFT))){
		text = Sys_GetClipboardText();
		if (!text)
			return;

		// Insert at the current position
		for (j = 0; text[j]; j++){
			if (text[j] < ' ')
				continue;	// Non printable

			if (con.overstrikeMode){
				if (con.inputPos == con.inputLen){
					if (con.inputLen == CON_MAX_INPUT - 1)
						break;

					con.inputLen++;
				}
			}
			else {
				if (con.inputLen == CON_MAX_INPUT - 1)
					break;

				for (i = con.inputLen; i > con.inputPos; i--)
					con.inputText[i] = con.inputText[i-1];

				con.inputLen++;
			}

			con.inputText[con.inputPos++] = text[j];
			con.inputText[con.inputLen] = 0;
		}

		Mem_Free(text);

		return;
	}

	// Toggle overstrike mode
	if (key == K_INS){
		con.overstrikeMode = !con.overstrikeMode;

		return;
	}

	// Scroll up
	if (key == K_PGUP || key == K_MWHEELUP){
		if (Key_IsDown(K_LCTRL) || Key_IsDown(K_RCTRL))
			con.displayLine -= 6;
		else
			con.displayLine -= 2;

		if (con.displayLine < con.currentLine - CON_LINES + 1)
			con.displayLine = con.currentLine - CON_LINES + 1;
		if (con.displayLine < CON_LINES)
			con.displayLine = CON_LINES;

		return;
	}

	// Scroll down
	if (key == K_PGDN || key == K_MWHEELDOWN){
		if (Key_IsDown(K_LCTRL) || Key_IsDown(K_RCTRL))
			con.displayLine += 6;
		else
			con.displayLine += 2;

		if (con.displayLine > con.currentLine)
			con.displayLine = con.currentLine;

		return;
	}

	// Scroll to first line
	if (key == K_HOME && (Key_IsDown(K_LCTRL) || Key_IsDown(K_RCTRL))){
		con.displayLine = con.currentLine - CON_LINES + 1;

		if (con.displayLine < CON_LINES)
			con.displayLine = CON_LINES;
		
		return;
	}

	// Scroll to last line
	if (key == K_END && (Key_IsDown(K_LCTRL) || Key_IsDown(K_RCTRL))){
		con.displayLine = con.currentLine;

		return;
	}

	// Execute a command
	if (key == K_ENTER){
		Com_Printf("]%s\n", con.inputText);

		if (!con.inputText[0])
			return;

		// Add the command text
		Cmd_AppendText(con.inputText);
		Cmd_AppendText("\n");

		// Add to history and clear input
		Str_Copy(con.historyText[con.historyCount], con.inputText, sizeof(con.historyText[con.historyCount]));
		con.historyCount = (con.historyCount + 1) & (CON_MAX_HISTORY-1);
		con.historyLine = con.historyCount;

		con.inputText[0] = 0;
		con.inputPos = 0;
		con.inputLen = 0;

		return;
	}

	// Go to previous history line
	if (key == K_UPARROW){
		do {
			con.historyLine = (con.historyLine - 1) & (CON_MAX_HISTORY-1);
		} while (con.historyLine != con.historyCount && !con.historyText[con.historyLine][0]);

		if (con.historyLine == con.historyCount)
			con.historyLine = (con.historyLine - con.historyCount) & (CON_MAX_HISTORY-1);

		Str_Copy(con.inputText, con.historyText[con.historyLine], sizeof(con.inputText));
		con.inputLen = Str_Length(con.inputText);
		con.inputPos = con.inputLen;

		return;
	}

	// Go to next history line
	if (key == K_DOWNARROW){
		if (con.historyLine == con.historyCount)
			return;

		do {
			con.historyLine = (con.historyLine + 1) & (CON_MAX_HISTORY-1);
		} while (con.historyLine != con.historyCount && !con.historyText[con.historyLine][0]);

		if (con.historyLine == con.historyCount){
			con.inputText[0] = 0;
			con.inputPos = 0;
			con.inputLen = 0;
		}
		else {
			Str_Copy(con.inputText, con.historyText[con.historyLine], sizeof(con.inputText));
			con.inputLen = Str_Length(con.inputText);
			con.inputPos = con.inputLen;
		}

		return;
	}

	// Go to previous word or character
	if (key == K_LEFTARROW){
		if (Key_IsDown(K_LCTRL) || Key_IsDown(K_RCTRL)){
			for (i = con.inputPos - 1; i >= 0; i--){
				if (con.inputText[i] != ' ')
					break;
			}

			for ( ; i >= 0; i--){
				if (con.inputText[i] == ' ')
					break;
			}

			con.inputPos = i + 1;
		}
		else {
			if (con.inputPos)
				con.inputPos--;
		}

		return;
	}

	// Go to next word or character
	if (key == K_RIGHTARROW){
		if (Key_IsDown(K_LCTRL) || Key_IsDown(K_RCTRL)){
			for (i = con.inputPos; i < con.inputLen; i++){
				if (con.inputText[i] == ' ')
					break;
			}

			for ( ; i < con.inputLen; i++){
				if (con.inputText[i] != ' ')
					break;
			}

			con.inputPos = i;
		}
		else {
			if (con.inputPos < con.inputLen)
				con.inputPos++;
		}

		return;
	}

	// Go to first character
	if (key == K_HOME){
		con.inputPos = 0;

		return;
	}

	// Go to last character
	if (key == K_END){
		con.inputPos = con.inputLen;

		return;
	}

	// Delete previous character
	if (key == K_BACKSPACE){
		if (!con.inputPos)
			return;

		con.inputPos--;

		for (i = con.inputPos; i < con.inputLen; i++)
			con.inputText[i] = con.inputText[i+1];

		con.inputLen--;

		return;
	}

	// Delete next character
	if (key == K_DEL){
		if (con.inputPos == con.inputLen)
			return;

		for (i = con.inputPos; i < con.inputLen; i++)
			con.inputText[i] = con.inputText[i+1];

		con.inputLen--;

		return;
	}
}

/*
 ==================
 Con_CharEvent
 ==================
*/
void Con_CharEvent (int c){

	const char	*text;
	int			i, j;

	if (!con.initialized)
		return;

	// Paste from clipboard
	if (c == 'v' - 'a' + 1){
		text = Sys_GetClipboardText();
		if (!text)
			return;

		// Insert at the current position
		for (j = 0; text[j]; j++){
			if (text[j] < ' ')
				continue;	// Non printable

			if (con.overstrikeMode){
				if (con.inputPos == con.inputLen){
					if (con.inputLen == CON_MAX_INPUT - 1)
						break;

					con.inputLen++;
				}
			}
			else {
				if (con.inputLen == CON_MAX_INPUT - 1)
					break;

				for (i = con.inputLen; i > con.inputPos; i--)
					con.inputText[i] = con.inputText[i-1];

				con.inputLen++;
			}

			con.inputText[con.inputPos++] = text[j];
			con.inputText[con.inputLen] = 0;
		}

		Mem_Free(text);

		return;
	}

	// Clear buffer
	if (c == 'l' - 'a' + 1){
		for (i = 0; i < CON_LINES; i++){
			for (j = 0; j < CON_LINELEN; j++)
				con.text[i][j] = 0;
		}

		con.currentLine = CON_LINES - 1;
		con.displayLine = CON_LINES - 1;

		return;
	}

	// Clear input
	if (c == 'c' - 'a' + 1){
		con.inputText[0] = 0;
		con.inputPos = 0;
		con.inputLen = 0;

		return;
	}

	// Normal typing
	if (c < ' ')
		return;		// Non printable

	if (con.overstrikeMode){
		if (con.inputPos == con.inputLen){
			if (con.inputLen == CON_MAX_INPUT - 1)
				return;

			con.inputLen++;
		}
	}
	else {
		if (con.inputLen == CON_MAX_INPUT - 1)
			return;

		for (i = con.inputLen; i > con.inputPos; i--)
			con.inputText[i] = con.inputText[i-1];

		con.inputLen++;
	}

	con.inputText[con.inputPos++] = c;
	con.inputText[con.inputLen] = 0;
}


/*
 ==============================================================================

 DRAWING

 ==============================================================================
*/


/*
 ==================
 Con_DrawChar
 ==================
*/
static void Con_DrawChar (int x, int y, int c){

	float	col, row;

	if (c == ' ')
		return;

	col = (c & 15) * 0.0625f;
	row = (c >> 4) * 0.0625f;

	R_DrawStretchPic(x, y, 8, 16, col, row, col + 0.0625f, row + 0.0625f, cls.charsetMaterial);
}

/*
 ==================
 Con_DrawNotify

 Draws the last few lines of output transparently on the screen
 ==================
*/
static void Con_DrawNotify (void){

	short	*text;
	int		colorIndex, oldColorIndex;
	int		time;
	int		c, x, y = 0;
	int		i, j;

	for (i = con.currentLine - CON_TIMES + 1; i <= con.currentLine; i++){
		time = con.notifyTimes[i % CON_TIMES];
		if (time == 0)
			continue;

		if (cls.frameTime - time > SEC2MS(con_notifyTime->floatValue))
			continue;

		// Draw the text
		text = con.text[i % CON_LINES];

		oldColorIndex = COLOR_DEFAULT;
		R_SetColor(colorTable[COLOR_DEFAULT]);

		for (j = 0, x = 8; j < CON_LINELEN; j++, x += 8){
			c = text[j] & 0xFF;
			if (!c)
				break;

			if (c == ' ')
				continue;

			colorIndex = (text[j] >> 8) & COLOR_MASK;
			if (colorIndex != oldColorIndex){
				oldColorIndex = colorIndex;
				R_SetColor(colorTable[colorIndex]);
			}

			Con_DrawChar(x, y, c);
		}

		y += 16;
	}
}

/*
 ==================
 Con_DrawInput

 The input line scrolls horizontally if typing goes beyond the right edge
 ==================
*/
static void Con_DrawInput (void){

	char	*text = con.inputText;
	odVec4	color;
	int		cursor, scroll, len = 0;
	int		width = CON_LINELEN - 2;
	int		ofs;
	int		i;

	ofs = (SCREEN_HEIGHT * con.displayFrac) - 36;

	// Draw the input prompt
	R_SetColor(colorTable[COLOR_DEFAULT]);
	Con_DrawChar(8, ofs, ']');

	// Find the cursor position, ignoring color sequences
	cursor = con.inputPos;

	for (i = 0; i < con.inputPos; i++){
		if (!Str_IsColor(&text[i]))
			continue;

		if (i == con.inputPos - 1)
			cursor -= 1;
		else
			cursor -= 2;
	}

	// Prestep if horizontally scrolling
	if (cursor <= width)
		scroll = 0;
	else {
		scroll = cursor - width;

		cursor = width;
	}

	R_SetColor(colorTable[COLOR_WHITE]);

	while (*text && scroll){
		if (Str_IsColor(text)){
			Str_GetColor(text[1], color);
			R_SetColor(color);

			text += 2;
			continue;
		}

		text++;
		scroll--;
	}

	// Draw the input text
	if (con.inputPos != con.inputLen)
		width++;

	while (*text && len < width){
		if (Str_IsColor(text)){
			Str_GetColor(text[1], color);
			R_SetColor(color);

			text += 2;
			continue;
		}

		Con_DrawChar(16 + (len * 8), ofs, *text);

		text++;
		len++;
	}

	// Draw the cursor if desired
	if ((cls.frameTime >> 8) & 1){
		R_SetColor(colorTable[COLOR_WHITE]);

		if (con.overstrikeMode)
			Con_DrawChar(16 + (cursor * 8), ofs, 11);
		else
			Con_DrawChar(16 + (cursor * 8), ofs, 10);
	}
}

/*
 ==================
 Con_DrawConsole
 ==================
*/
void Con_DrawConsole (void){

	short	*text;
	int		colorIndex, oldColorIndex;
	int		lines, line;
	int		height, len;
	int		c, x, y;
	int		i;

	if (!con.initialized)
		return;

	// Force it to full screen if nothing is drawn behind
	if (cls.state != CS_ACTIVE && !(Key_GetCatchers() & KEYCATCH_GUI))
		con.displayFrac = 1.0f;

	// If not visible, draw notify lines
	if (!con.displayFrac){
		if (con_noPrint->integerValue)
			return;

		Con_DrawNotify();
		return;
	}

	height = SCREEN_HEIGHT * con.displayFrac;

	// Clear screen to black if nothing is drawn behind
	if (con.displayFrac == 1.0f){
		R_SetColor(colorTable[COLOR_BLACK]);
		R_DrawStretchPic(0.0f, 0.0f, SCREEN_WIDTH, SCREEN_HEIGHT, 0.0f, 0.0f, 1.0f, 1.0f, cls.whiteMaterial);
	}

	// Draw the background
	R_SetColor(colorTable[COLOR_WHITE]);
	R_DrawStretchPic(0.0f, 0.0f, SCREEN_WIDTH, height, 0.0f, 0.0f, 1.0f, 1.0f, cls.consoleMaterial);

	// Draw the border
	R_SetColor(colorTable[COLOR_DEFAULT]);
	R_DrawStretchPic(0.0f, height - 2, SCREEN_WIDTH, 2.0f, 0.0f, 0.0f, 1.0f, 1.0f, cls.whiteMaterial);

	// Draw the version string
	len = Str_Length(ENGINE_VERSION);

	x = SCREEN_WIDTH - (len + 1) * 8;
	y = height - 20;

	for (i = 0; i < len; i++, x += 8)
		Con_DrawChar(x, y, ENGINE_VERSION[i]);

	y -= 28;

	// Draw arrows to show the buffer is backscrolled
	if (con.displayLine != con.currentLine){
		for (i = 0, x = 8; i < CON_LINELEN; i += 4, x += 32)
			Con_DrawChar(x, y, '^');
	}

	y -= 16;

	// Draw the text from the top down
	lines = (y / 16) + 1;
	y -= (lines * 16);

	oldColorIndex = COLOR_DEFAULT;
	R_SetColor(colorTable[COLOR_DEFAULT]);

	for (line = con.displayLine - lines; line <= con.displayLine; line++, y += 16){
		if (line < 0 || con.currentLine - line >= CON_LINES)
			continue;
		if (y <= -16)
			continue;

		text = con.text[line % CON_LINES];

		for (i = 0, x = 8; i < CON_LINELEN; i++, x += 8){
			c = text[i] & 0xFF;
			if (!c)
				break;

			if (c == ' ')
				continue;

			colorIndex = (text[i] >> 8) & COLOR_MASK;
			if (colorIndex != oldColorIndex){
				oldColorIndex = colorIndex;
				R_SetColor(colorTable[colorIndex]);
			}

			Con_DrawChar(x, y, c);
		}
	}

	// Draw the input prompt, text, and cursor if desired
	Con_DrawInput();
}


// ============================================================================


/*
 ==================
 Con_ToggleConsole_f
 ==================
*/
void Con_ToggleConsole_f (void){

	if (!con.initialized)
		return;

	Con_ClearNotify();
	Con_ClearInput();

	if (Key_GetCatchers() & KEYCATCH_CONSOLE)
		Key_RemoveCatchers(KEYCATCH_CONSOLE);
	else
		Key_SetCatchers(KEYCATCH_CONSOLE);
}

/*
 ==================
 Con_Clear_f
 ==================
*/
static void Con_Clear_f (void){

	int		i, j;

	for (i = 0; i < CON_LINES; i++){
		for (j = 0; j < CON_LINELEN; j++)
			con.text[i][j] = 0;
	}

	con.currentLine = CON_LINES - 1;
	con.displayLine = CON_LINES - 1;
}

/*
 ==================
 Con_Dump_f

 Save the console contents out to a file
 ==================
*/
static void Con_Dump_f (void){

	fileHandle_t	f;
	char			name[MAX_OSPATH];
	char			line[CON_LINELEN + 1];
	short			*text;
	int				i, j;

	if (Cmd_Argc() != 2){
		Com_Printf("Usage: conDump <fileName>\n");
		return;
	}

	Str_Copy(name, Cmd_Argv(1), sizeof(name));
	Str_DefaultFileExtension(name, sizeof(name), ".txt");

	FS_OpenFile(name, &f, FS_WRITE);
	if (!f){
		Com_Printf("Couldn't write %s\n", name);
		return;
	}

	Com_Printf("Dumped console text to %s\n", name);

	// Skip empty lines
	for (i = con.currentLine - CON_LINES + 1; i <= con.currentLine; i++){
		if (con.text[i % CON_LINES][0] & 0xFF)
			break;
	}

	// Write the remaining lines
	for ( ; i <= con.currentLine; i++){
		text = con.text[i % CON_LINES];

		for (j = 0; j < CON_LINELEN; j++){
			line[j] = text[j] & 0xFF;

			if (!line[j])
				break;
		}

		line[j] = 0;

		FS_Printf(f, "%s" LINE_SEPARATOR, line);
	}

	FS_CloseFile(f);
}

/*
 ==================
 Con_Init
 ==================
*/
void Con_Init (void){

	// Register our variables and commands
	con_notifyTime = CVar_Get("con_notifyTime", "3.0", 0, "Time, in seconds, to display notify messages on-screen");
	con_speed = CVar_Get("con_speed", "3.0", 0, "Speed, in seconds, at which the console is toggled on/off");
	con_noPrint = CVar_Get("con_noPrint", "1", 0, "Don't print console messages on-screen");

	Cmd_AddCommand("toggleConsole", Con_ToggleConsole_f, "Toggles the console on/off");
	Cmd_AddCommand("clear", Con_Clear_f, "Clears the console text");
	Cmd_AddCommand("conDump", Con_Dump_f, "Dumps the console text to a file");

	con.currentLine = CON_LINES - 1;
	con.displayLine = CON_LINES - 1;
	con.colorIndex = COLOR_DEFAULT;

	con.initialized = true;

	Com_Printf("Console Initialized\n");
}

/*
 ==================
 Con_Shutdown
 ==================
*/
void Con_Shutdown (void){

	int		i;

	if (!con.initialized)
		return;

	Cmd_RemoveCommand("toggleConsole");
	Cmd_RemoveCommand("clear");
	Cmd_RemoveCommand("conDump");

	// Free the auto-complete list
	for (i = 0; i < con.cmdCount; i++)
		Mem_Free(con.cmdList[i]);

	memset(&con, 0, sizeof(console_t));
}
